//  -----------------------------------------------------------------------------------------------//
//   This file is part of SvnExternalUpdater                                                               //
//   Copyright (C)2008 Romain Verdier ( romain@codingly.com )                                      //
//                                                                                                 //
//   This program is free software; you can redistribute it and/or modify it under the terms       //
//   of the GNU General Public License as published by the Free Software Foundation; either        //
//   version 2 of the License, or (at your option) any later version. This program is distributed  //
//   in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied        //
//   warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public  //
//   License for more details.                                                                     //
//                                                                                                 //
//   You should have received a copy of the GNU General Public License along with this program; if //
//   not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. or  //
//   see http://www.fsf.org/licensing/licenses/gpl.html                                            //
//  -----------------------------------------------------------------------------------------------//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Timers;
using System.Windows.Forms;
using Codingly.SvnExternalUpdater.Core.EventHandlers;
using Codingly.SvnExternalUpdater.Core.Model;
using Codingly.SvnExternalUpdater.Core.Services;
using Timer=System.Timers.Timer;

namespace Codingly.SvnExternalUpdater.Core
{
    public class MonitoringService : IMonitoringService, IRemoteFolderProvider
    {
        private readonly Cache<External> externalCache = new Cache<External>();
        private readonly object lockObject = new object();
        private readonly Cache<Project> projectCache = new Cache<Project>();
        private readonly Dictionary<Uri, IRemoteFolder> remoteFolders = new Dictionary<Uri, IRemoteFolder>();
        private readonly Timer timer = new Timer();
        private readonly IVcsService vcsService;

        private bool? externalAreUpToDate;
        private bool? projectsAreUpToDate;

        public MonitoringService(IVcsService vcsService)
        {
            this.vcsService = vcsService;
            this.timer.Elapsed += timer_Elapsed;
        }

        private void CleanProjects(ICollection<string> projectPaths)
        {
            foreach (Project project in this.projectCache.All)
            {
                if (!projectPaths.Contains(project.Path))
                {
                    this.projectCache.Remove(project.Path);
                    ProjectRemoved(this, new ProjectEventArgs(project));
                }
            }
        }

        private void CleanRemoteFolders()
        {
            var toDelete = new List<Uri>();
            foreach (KeyValuePair<Uri, IRemoteFolder> pair in this.remoteFolders)
            {
                if (this.projectCache.Contains(pair.Value) || this.externalCache.Contains(pair.Value))
                {
                    continue;
                }
                toDelete.Add(pair.Key);
            }
            foreach (Uri uri in toDelete)
            {
                this.remoteFolders.Remove(uri);
            }
        }

        private void CreateNewProjects(IEnumerable<string> projectPaths)
        {
            foreach (string path in projectPaths)
            {
                if (this.projectCache.Contains(path))
                {
                    continue;
                }

                try
                {
                    Project project = CreateProject(path);
                    if (project == null)
                    {
                        continue;
                    }
                    
                    this.projectCache.Add(project);
                    ProjectAdded(this, new ProjectEventArgs(project));
                }
                catch (System.Exception e)
                {
                    MessageBox.Show(e.ToString(), "Can't not create project: " + path);
                }

            }
        }

        private Project CreateProject(string path)
        {
            LocalFolderInfo infos = this.vcsService.GetLocalDirectoryInfo(path);
            if (infos == null)
            {
                return null;
            }

            Project project = ProjectFactory.Create(path, infos, this);
            project.IsUpToDate = this.vcsService.IsUpToDate(path);
            UpdateProjectExternals(project, infos);

            return project;
        }

        private void RemoveOldProjects(IEnumerable<Project> toDelete)
        {
            foreach (Project project in toDelete)
            {
                this.projectCache.Remove(project.Path);
                ProjectRemoved(this, new ProjectEventArgs(project));
            }
        }

        private void Update()
        {
            if (!Monitor.TryEnter(this.lockObject))
            {
                return;
            }

            try
            {
                UpdateStarted(this, EventArgs.Empty);
                UpdateProjects();
                UpdateStatus();
                UpdateStopped(this, EventArgs.Empty);
            }
            finally
            {
                Monitor.Exit(this.lockObject);
            }
        }

        private void UpdateProjectExternals(Project project, LocalFolderInfo infos)
        {
            project.ExternalPropertyValue = infos.ExternalPropertyValue;

            var toRemove = new List<IExternal>();
            foreach (IExternal external in project.Externals)
            {
                this.externalCache.Remove(external.Path);
                toRemove.Add(external);
            }

            foreach (IExternal external in toRemove)
            {
                project.RemoveExternal(external);
            }

            foreach (string externalReference in infos.ExternalReferences)
            {
                External external = ExternalFactory.Create(externalReference, project, infos.RepositoryRootUrl, this);
                this.externalCache.Add(external);
                project.AddExternal(external);
            }
        }

        private void UpdateProjects()
        {
            IEnumerable<Project> updatedProjectsFromLocalInfos = UpdateProjectsFromLocalInfos();
            IEnumerable<Project> updatedProjectsFromRemoteInfos = UpdateProjectsFromRemoteInfos();
            foreach (Project project in updatedProjectsFromLocalInfos.Union(updatedProjectsFromRemoteInfos))
            {
                project.FireChanged();
            }
        }

        private IEnumerable<Project> UpdateProjectsFromLocalInfos()
        {
            var toDelete = new List<Project>();
            var toCreate = new List<string>();
            var updatedProjects = new List<Project>();
            foreach (Project project in this.projectCache.All)
            {
                LocalFolderInfo infos = this.vcsService.GetLocalDirectoryInfo(project.Path);
                if (project.Url != infos.Url)
                {
                    toDelete.Add(project);
                    toCreate.Add(project.Path);
                    continue;
                }


                if (ExternalPropertyValuesAreDifferent(project.ExternalPropertyValue, infos.ExternalPropertyValue))
                {
                    UpdateProjectExternals(project, infos);
                    updatedProjects.Add(project);
                }
            }

            RemoveOldProjects(toDelete);
            CreateNewProjects(toCreate);
            CleanRemoteFolders();

            return updatedProjects;
        }

        private IEnumerable<Project> UpdateProjectsFromRemoteInfos()
        {
            var updatedProjects = new List<Project>();
            foreach (RemoteFolder remoteFolder in this.remoteFolders.Values)
            {
                long newRevision = this.vcsService.GetLastChangeRevision(remoteFolder.Url);
                if (newRevision != remoteFolder.Revision)
                {
                    remoteFolder.Revision = newRevision;
                    IEnumerable<Project> projects = this.projectCache.GetByRemoteFolder(remoteFolder);
                    foreach (Project project in projects)
                    {
                        updatedProjects.Add(project);
                    }

                    IEnumerable<External> externals = this.externalCache.GetByRemoteFolder(remoteFolder);
                    foreach (External external in externals)
                    {
                        external.FireChanged();
                    }
                }
            }

            foreach (Project project in this.projectCache.All)
            {
                if (project.IsUpToDate)
                {
                    project.IsUpToDate = this.vcsService.IsUpToDate(project.Path);
                }
            }

            return updatedProjects;
        }

        private void UpdateStatus()
        {
            bool allIsUpToDate = this.projectCache.AllIsUpToDate;
            if (this.projectsAreUpToDate != allIsUpToDate)
            {
                ProjectStatusChanged(this, new StatusChangedEventArgs(allIsUpToDate));
                this.projectsAreUpToDate = allIsUpToDate;
            }

            allIsUpToDate = this.externalCache.AllIsUpToDate;
            if (this.externalAreUpToDate != allIsUpToDate)
            {
                ExternalStatusChanged(this, new StatusChangedEventArgs(allIsUpToDate));
                this.externalAreUpToDate = allIsUpToDate;
            }
        }

        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            Update();
        }

        #region IMonitoringService Members

        public void Configure(IList<string> projectPaths, int interval)
        {
            ThreadPool.QueueUserWorkItem(delegate
                                         {
                                            UpdateStarted(this, EventArgs.Empty);
                                            this.timer.Interval = interval*1000;
                                            CleanProjects(projectPaths);
                                            CreateNewProjects(projectPaths);
                                            CleanRemoteFolders();
                                            UpdateStopped(this, EventArgs.Empty);
                                         });
        }

        public event StatusChangedEventHandler ExternalStatusChanged = delegate { };
        public event ProjectEventHandler ProjectAdded = delegate { };
        public event ProjectEventHandler ProjectRemoved = delegate { };
        public event StatusChangedEventHandler ProjectStatusChanged = delegate { };
        public event EventHandler UpdateStarted = delegate { };
        public event EventHandler UpdateStopped = delegate { };

        public void UpdateExternalRevision(IExternal external)
        {
            // Todo : change design to avoid this cast
            var toUpdate = external as External;
            if (toUpdate == null)
            {
                return;
            }

            var project = toUpdate.Project as Project;
            if (project == null)
            {
                return;
            }

            toUpdate.UpdateLocalRevision();
            project.UpdateExternalPropertyValue();
            this.vcsService.SetExternalPropertyValue(project.Path, project.ExternalPropertyValue);
            toUpdate.FireChanged();
        }

        public void Stop()
        {
            this.timer.Stop();
        }

        public void Start()
        {
            this.timer.Start();
            Update();
        }

        public void Refresh()
        {
            ThreadPool.QueueUserWorkItem(delegate { Update(); });
        }

        #endregion

        #region IRemoteFolderProvider Members

        public IRemoteFolder GetOrCreateRemoteFolder(Uri url)
        {
            IRemoteFolder remoteFolder;
            if (!this.remoteFolders.TryGetValue(url, out remoteFolder))
            {
                remoteFolder = new RemoteFolder {Revision = this.vcsService.GetLastChangeRevision(url), Url = url};
                this.remoteFolders.Add(url, remoteFolder);
            }
            return remoteFolder;
        }

        #endregion

        private static bool ExternalPropertyValuesAreDifferent(string fromProject, string fromInfos)
        {
            if (fromProject != null && fromInfos != null)
            {
                fromProject = fromProject.Replace("\r\r", "\r");
                fromInfos = fromInfos.Replace("\r\r", "\r");
            }

            return fromProject != fromInfos;
        }
    }
}